package ch.hslu.gl20vscpudemo;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import ch.hslu.gl20vscpudemo.FrameBase.Config.BitmapColor;
import ch.hslu.gl20vscpudemo.FrameBase.Config.ProcessingUnit;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.util.Log;
import android.view.SurfaceView;

/**
 * Frame Base Class
 * Choose a configuration and render it with the target platform
 */
public class FrameBase implements GLES20Renderer.Callback {

	// interfaces ////////////////////////////////////////////////////////////////
	interface Config {
		enum ProcessingUnit {
			CPU, CPUFPU, GPU
		}

		enum BitmapColor {
			RGB, GREY
		}
	}
	
	interface Callback {
		public void rendererTime(long timeMs);
	}
	
	// members ///////////////////////////////////////////////////////////////////
	private final Context mContext;

	// surfaces
	private GLES20SurfaceView mGLES20sv;
	private AsyncSurfaceView mAsv;

	// image
	private int nBmpWidth;
	private int nBmpHeight;
	private Bitmap mBmp;
	private ByteBuffer mBbBmp;
	private int[] mIntBmp;

	// configuration
	private ProcessingUnit eRenderTarget;
	private BitmapColor eBitmapColor;
	
	// Kernels
	private float[][] a2fKernel;
	private float[][] a2f255Kernel;
	private int[][] a2nKernel;
	
	// measurement
	private long nCurrTimeMs;
	private long nResultTimeMs;
	private Callback mCallback;

	// constructor ///////////////////////////////////////////////////////////////
	/**
	 * initialize the base of CPU/GPU renderer
	 * @param context activity
	 */
	public FrameBase(Context context) {
		mContext = context;
		
		mCallback = null;

		// test if callback installed
		Method[] methodsCallback = Callback.class.getMethods();
		Method[] methodsContext = mContext.getClass().getMethods();
		for(int i=0; i < methodsContext.length; i++){
			if(methodsContext[i].getName().equals(methodsCallback[0].getName())){
				// callback is located
				mCallback = (Callback)mContext;
			}
		}


		// default states
		nBmpWidth = 0;
		nBmpHeight = 0;
		eRenderTarget = ProcessingUnit.GPU;
		eBitmapColor = BitmapColor.RGB;
		setGaussianKernel(1, 1);
}

	// public methods ////////////////////////////////////////////////////////////

	// bitmap handling
	/**
	 * push an image
	 * @param bmp image
	 */
	public void pushImage(Bitmap bmp) {
		storeImages(bmp);
	}

	/**
	 * push an image
	 * @param is image
	 */
	public void pushImage(InputStream is) {
		pushImage(BitmapFactory.decodeStream(is));
	}

	/**
	 * push an image
	 * @param ba image
	 */
	public void pushImage(byte[] ba) {
		final ByteArrayInputStream bais = new ByteArrayInputStream(ba);
		pushImage((InputStream) bais);
	}

	/**
	 * get the result image
	 * @return Image
	 */
	public Bitmap getBitmap() {
		return mBmp;
	}
	
	// configuration handling
	/**
	 * set bitmap color configuration
	 * @param bmpCol color
	 * @see Config
	 */
	public void setBitmapColor(BitmapColor bmpCol) {
		eBitmapColor = bmpCol;
	}

	/**
	 * set renderer processing unit
	 * @param processingUnit the target unit
	 * @see Config
	 */
	public void setProcessingUnit(ProcessingUnit processingUnit) {
		eRenderTarget = processingUnit;
	}
	
	/**
	 * set gaussian kernel
	 * @param size kernel size
	 * @param sigma gaussian sigma
	 */
	public void setGaussianKernel(int size, double sigma){
		a2fKernel = new float[size][size];
		GaussKernel.calcMatrix(a2fKernel, sigma, 1.0f);
		a2nKernel = new int[size][size];
		GaussKernel.calcMatrix(a2nKernel, sigma, 255);
		a2f255Kernel = new float[size][size];
		GaussKernel.calcMatrix(a2f255Kernel, sigma, 255);
	}
	
	/**
	 * set a custom kernel
	 * @param kernel kernel with custom values
	 */
	public void setCustomKernel(int[][] kernel){
		a2fKernel = new float[kernel.length][kernel[0].length];
		a2nKernel = new int[kernel.length][kernel[0].length];
		a2f255Kernel = new float[kernel.length][kernel[0].length];
		for(int y = 0; y < kernel.length; y++){
			for(int x = 0; x < kernel[0].length; x++){
				a2fKernel[y][x] = (float)kernel[y][x];
				a2f255Kernel[y][x] = (float)kernel[y][x] * 255.0f;
				a2nKernel[y][x] = kernel[y][x] * 255;
			}
		}
	}
	
	// surface handling
	/**
	 * get a surface, this call start the renderer process on the target unit and get the right surface
	 * @return SurfaceView
	 */
	public SurfaceView getSurface() {
		mGLES20sv = null;
		mAsv = null;
		if ((0 < nBmpWidth) && (0 < nBmpHeight)) {
			switch (eRenderTarget) {
				case GPU:
					mGLES20sv = new GLES20SurfaceView(mContext, (GLES20Renderer.Callback)this, nBmpWidth, nBmpHeight, a2fKernel);
					mGLES20sv.pushImage(mBbBmp);
					nCurrTimeMs = System.currentTimeMillis(); // start measure
					return (SurfaceView) mGLES20sv;
				case CPUFPU :
					mAsv = new AsyncSurfaceView(mContext, nBmpWidth, nBmpHeight);
					nCurrTimeMs = System.currentTimeMillis(); // start measure
					mBmp = CpuRenderer.applyKernel(mIntBmp, nBmpWidth, nBmpHeight, a2f255Kernel);
					checkAndCallback(System.currentTimeMillis() - nCurrTimeMs); // stop measure
					mAsv.pushImage(mBmp);
					return (SurfaceView) mAsv;
				case CPU:
				default:
					mAsv = new AsyncSurfaceView(mContext, nBmpWidth, nBmpHeight);
					nCurrTimeMs = System.currentTimeMillis(); // start measure
					mBmp = CpuRenderer.applyKernel(mIntBmp, nBmpWidth, nBmpHeight, a2nKernel);
					checkAndCallback(System.currentTimeMillis() - nCurrTimeMs); // stop measure
					mAsv.pushImage(mBmp);
					return (SurfaceView) mAsv;
			}
		} else {
			Log.e(this.toString(), "push image first");
			return null;
		}
	}
	
	/**
	 * after getRenderer(), measure the time
	 * @return
	 */
	public long getRenderTime(){
		return nResultTimeMs;
	}
	
	/** GLES20Renderer.Callback */
	public void onBitmap(Bitmap bmp) {
		checkAndCallback(System.currentTimeMillis() - nCurrTimeMs); // store cpu measure
		if (null != bmp) {
			mBmp = Bitmap.createBitmap(bmp);
		}
	}

	// private methods ///////////////////////////////////////////////////////////
	/**
	 * convert Bitmap to ByteArray
	 * @param bmp image
	 * @return image
	 */
	private final byte[] bmpToByteArray(final Bitmap bmp) {
		final int w = bmp.getWidth();
		final int h = bmp.getHeight();
		final byte[] buf = new byte[w * h * 3];
		for (int y = 0; y < h; y++) {
			for (int x = 0; x < w; x++) {
				final int pixel = bmp.getPixel(x, y);
				buf[(((y * w) + x) * 3) + 0] = (byte) ((pixel >> 16) & 0xFF); // red
				buf[(((y * w) + x) * 3) + 1] = (byte) ((pixel >> 8) & 0xFF); // green
				buf[(((y * w) + x) * 3) + 2] = (byte) ((pixel >> 0) & 0xFF); // blue
			}
		}
		return buf;
	}

	/**
	 * convert bitmap to ByteBuffer
	 * @param bmp image
	 * @return image
	 */
	private final ByteBuffer bmpToByteBuffer(Bitmap bmp) {
		final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bmp.getWidth() * bmp.getHeight() * 3);
		byteBuffer.put(bmpToByteArray(bmp)).position(0);
		return byteBuffer;
	}
	
	/**
	 * convert bitmap to int array
	 * @param bmp image
	 * @return image
	 */
	private final int[] bmpToIntArray(Bitmap bmp){
		final int w = bmp.getWidth();
		final int h = bmp.getHeight();
		final int[] buf = new int[w * h * 3];
		for (int y = 0; y < h; y++) {
			for (int x = 0; x < w; x++) {
				final int pixel = bmp.getPixel(x, y);
				buf[(((y * w) + x) * 3) + 0] = ((pixel >> 16) & 0xFF); // red
				buf[(((y * w) + x) * 3) + 1] = ((pixel >> 8) & 0xFF); // green
				buf[(((y * w) + x) * 3) + 2] = ((pixel >> 0) & 0xFF); // blue
			}
		}
		return buf;
	}

	/**
	 * convert image to grey
	 * @param btm image
	 * @return image
	 */
	private final Bitmap rgbToGrey(Bitmap btm) {
		// rgb to grey
		final ColorMatrix cm = new ColorMatrix();
		cm.setSaturation(0);
		final ColorMatrixColorFilter cmcf = new ColorMatrixColorFilter(cm);
		final Paint p = new Paint();
		p.setColorFilter(cmcf);
		final Bitmap bmpGrey = Bitmap.createBitmap(btm.getWidth(), btm.getHeight(), Bitmap.Config.RGB_565);
		final Canvas c = new Canvas(bmpGrey);
		c.drawBitmap(btm, 0, 0, p);
		return bmpGrey;
	}

	/**
	 * store an image into the right type for CPU and GPU
	 * @param bmp
	 */
	private void storeImages(Bitmap bmp) {
		nBmpWidth = bmp.getWidth();
		nBmpHeight = bmp.getHeight();
		// at this point: choose color
		switch (eBitmapColor) {
			case GREY:
				mBmp = rgbToGrey(bmp);
				break;
			case RGB:
			default:
				mBmp = bmp;
				break;
		}
		mBbBmp = bmpToByteBuffer(mBmp);
		mIntBmp = bmpToIntArray(mBmp);
	}
	
	/**
	 * is needed for gpu measure
	 * @param timeMs
	 */
	private void checkAndCallback(long timeMs){
		if(null != mCallback){
			mCallback.rendererTime(timeMs);
		}
	}
}
